/*
 * GEmilErpelKO3MoveMaker.java
 * Created on 12 sept. 2008
 *
 * Glacéo Internet Platform
 * http://sourceforge.net/projects/chm/
 *
 * Copyright (c) 2005-2008, Jens and Jan Janke (VirtualHockey Project)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.hockeo.vhbase.seasoncycle.movemaking.impl;


import java.util.Map;
import java.util.Random;

import com.hockeo.vhbase.conf.GRulesConstants;
import com.hockeo.vhbase.data.dao.club.GClub;
import com.hockeo.vhbase.data.dao.match.GMatch;
import com.hockeo.vhbase.data.dao.standings.GGoalsAccount;
import com.hockeo.vhbase.error.GException;
import com.hockeo.vhbase.seasoncycle.movemaking.GAbstractMoveMaker;

/**
 * This class is responsible for making moves for computer controlled clubs that rely on
 * the EmilErpel user implementation. It only handles KO best-of-three single direct
 * elimination style contest elements.
 *
 * @version $Id$
 * @author jjanke
 */
final public class GEmilErpelKO3MoveMaker extends GAbstractMoveMaker
{
  // Calculated data (once before goals are placed for the first match)
  //
  private boolean       d_fLastRound;
  private GGoalsAccount d_ga;
  private GGoalsAccount d_gaOpponent;

  // Values change for every match
  //
  private double        d_dAvailGoals;
  private double        d_dAvailGoalsOpponent;
  private int           d_nRemainingMatchesToWin;
  private int           d_nRemainingMatchesToWinOpp;
  private boolean       d_fMovesMade = false;

  @Override
  public void setup() throws GException
  {
    d_fLastRound = getNumMatchesLeft() == 0;

    // Load goal accounts
    //
    Map<GClub, GGoalsAccount> mapGAs = getDao().findGoalsAccountsForMatch( getMatches().get( 0 ) );
    d_ga = mapGAs.get( getClub() );
    d_gaOpponent = mapGAs.get( getMatches().get( 0 ).getOpponent( getClub() ) );
  }

  @Override
  public void computeMoves() throws GException
  {
    d_dAvailGoals = d_ga.getGoals().doubleValue();
    d_dAvailGoalsOpponent = d_gaOpponent.getGoals().doubleValue();
    d_nRemainingMatchesToWin = getNumMatchesLeft() + 2;
    d_nRemainingMatchesToWinOpp = d_nRemainingMatchesToWin;

    int iMatchToLose = getIndexOfMatchToLose();

    for ( int i = 0; i < getMatches().size(); i++ )
    {
      GMatch match = getMatches().get( i );

      makeMoveForMatch( match, i, i == iMatchToLose );

      if ( d_fMovesMade )
        break;

      if ( i < getMatches().size() - 1 )
        updateInternalState( match, getPlacedGoals( i ) );
    }
  }

  /**
   * Updates the internal state member variables so that the passed match for which goals
   * have previously been placed is considered as played.
   *
   * @param match a match for which goals have been placed
   * @param nGoals the number of placed goals
   */
  private void updateInternalState( final GMatch match, final int nGoals )
  {
    d_dAvailGoals -= match.getCostOfOneGoal( getClub() ).doubleValue() * nGoals;
  }

  /**
   * Places goals for the given match using the standard algorithm which treats every
   * match one by one and does not consider more than one match at a time. It is expected
   * that the internal state variables giving information about the goals account and the
   * remaining number of matches to be played are up-to-date. This method does not update
   * them at the end of the move.
   *
   * @param match the match for which to make the move
   * @param iMatch the index of the match in the list that contains all matches for which
   *          moves need to be made (d_listMatches)
   * @param fCanBeLost indicates if this match can/should be lost
   */
  private void makeMoveForMatch( final GMatch match, final int iMatch, final boolean fCanBeLost )
  {
    // If we do not have enough goals left, then we are quickly done
    //
    if ( d_dAvailGoals < match.getCostOfOneGoal( getClub() ).doubleValue() )
    {
      placeGoals( 1 );
      return;
    }

    // Determine maximum number of goals that can be scored in the current match
    //
    int nMaxGoalsForCurrentMatch = getNumMaxPossibleGoals( match, getClub(), d_dAvailGoals );

    // If it's the last match, then we place as many goals as we can
    //
    if ( d_fLastRound && iMatch == getMatches().size() - 1 )
    {
      placeGoals( nMaxGoalsForCurrentMatch );
      return;
    }

    // We can potentially make a very good move using a special algorithm for the last
    // round
    //
    if ( d_fLastRound && makeMoveLastRound() )
    {
      d_fMovesMade = true;
      return;
    }

    // Instantiate probability array
    //
    int[] anGoalProbabilities = new int[nMaxGoalsForCurrentMatch + 1];

    if ( fCanBeLost )
    {
      prepareOptimalMoveForExpectedLoss( match, anGoalProbabilities );
      d_nRemainingMatchesToWinOpp--;
    }
    else
    {
      prepareBestPossibleMovetoWin( match, anGoalProbabilities );
      d_nRemainingMatchesToWin--;
    }

    placeGoals( match, getClub(), anGoalProbabilities, nMaxGoalsForCurrentMatch );
  }

  /**
   * Tries to make an optimal move for the last round of the contest. If it is possible to
   * make an optimal move, moves for all three matches are made and <code>true</code> is
   * returned in order to inform the rest of the algorithm that the move-making process
   * can be stopped.
   */
  private boolean makeMoveLastRound()
  {
    // In the situation that we can score more than twice the goals than our opponent, we
    // can place the goals in such a way that a victory is certain. We always try to win
    // the first and the second match if we have only one game at home, otherwise the
    // first and the third one.
    //
    if ( d_dAvailGoals < d_dAvailGoalsOpponent * 2 )
      return false;

    boolean fTwoHomeMatches = false;
    for ( int i = 0; i < getMatches().size(); i++ )
    {
      GMatch match = getMatches().get( i );

      if ( i == 0 && getClub().equals( match.getClubHome() ) )
        fTwoHomeMatches = true;

      if ( i == 0 )
      {
        double dCostOneGoalOpp;

        if ( fTwoHomeMatches )
          dCostOneGoalOpp = GRulesConstants.COST_AWAY_GOAL.doubleValue();
        else
          dCostOneGoalOpp = GRulesConstants.COST_HOME_GOAL.doubleValue();

        double dGoalsOpp = Math.ceil( d_dAvailGoalsOpponent / dCostOneGoalOpp );
        double dGoals = dGoalsOpp + 1;

        placeGoals( (int) dGoals );
        d_dAvailGoals -= dGoals * match.getCostOfOneGoal( getClub() ).doubleValue();
      }
      else if ( i == 1 )
      {
        // If we have only one home match, than this the one to win
        //
        if ( !fTwoHomeMatches )
          placeGoals( (int) Math.ceil( d_dAvailGoals / GRulesConstants.COST_HOME_GOAL.doubleValue() ) );
        else
          placeGoals( 0 );
      }
      else if ( i == 2 )
      {
        // If we have two home matches, than this one should be won
        //
        if ( fTwoHomeMatches )
          placeGoals( (int) Math.ceil( d_dAvailGoals / GRulesConstants.COST_HOME_GOAL.doubleValue() ) );
        else
          placeGoals( 1 );
      }
    }

    return true;
  }

  /**
   * Initializes the probability array d_anGoalProbabilities in such a way that there is
   * quite high chance to score 0 or 1 goals in the match.
   *
   * @param match the match for which to prepare the array
   * @param anGoalProbabilities the current goal placing probabilities array
   */
  private void prepareOptimalMoveForExpectedLoss( final GMatch match, final int[] anGoalProbabilities )
  {
    // we create a gaussian distribution with an expected value of 0, the std. deviation
    // is set to 1
    //
    manipulateProbabilitiesGauss( anGoalProbabilities, 0, 1, 1 );
  }

  /**
   * Initializes the goals probability array in such a way, that the expected value of
   * goals is very likely to produce a positive (victory) match result.
   *
   * @param match the match for which to prepare the array
   * @param anGoalProbabilities the current goal placing probabilities array
   */
  private void prepareBestPossibleMovetoWin( final GMatch match, final int[] anGoalProbabilities )
  {
    // Average number of available goals per match (only matches to win (2 per round) are
    // considered)
    //
    final double dAvgGoalsPerMatch = d_dAvailGoals / d_nRemainingMatchesToWin;
    final double dAvgGoalsPerMatchOpp = d_dAvailGoalsOpponent / d_nRemainingMatchesToWinOpp;

    // Initialize array with own goals per match avg.
    //
    manipulateProbabilitiesGauss( anGoalProbabilities, dAvgGoalsPerMatch, 1.0, 1.0 );

    // Add the influence of the adversary's goals avg. Since the probability for a club to
    // score more goals at home than away is given, use a higher factor when we are
    // playing an away match.
    //
    double dFactor = 1.125;
    if ( match.getClubHome().equals( getClub() ) )
      dFactor = 1.0;

    manipulateProbabilitiesGauss( anGoalProbabilities, dAvgGoalsPerMatchOpp, 1.5, dFactor );
  }

  /**
   * Determines the index (between 0 and 2) of the match that should be given lost in the
   * currently played round.
   */
  private int getIndexOfMatchToLose()
  {
    int[] an = new int[getMatches().size()];
    int nLastVal = 0;
    int nAwayLosingProb;

    // The probability for the away match to be lost is different depending on the number
    // of away matches to play in the current round
    //
    if ( getMatches().get( 0 ).getClubHome().equals( getClub() ) )
      nAwayLosingProb = 6; // corresponds to a losing probab. for a home match of 2 / ( 1
    // + 1 + 6 ) = 25%
    else
      nAwayLosingProb = 4; // corresponds to a losing probab. for a home match of 1 / ( 1
    // + 4 + 4 ) = 11%

    for ( int i = 0; i < getMatches().size(); i++ )
    {
      // For each home match we assume a probability of 1, for away matches 2
      //
      an[i] = getMatches().get( i ).getClubHome().equals( getClub() )
          ? nLastVal + 1 : nLastVal + nAwayLosingProb;
      nLastVal = an[i];
    }

    // Determine random number
    //
    int nRand = new Random().nextInt( nLastVal + 1 );

    for ( int i = 0; i < an.length; i++ )
      if ( nRand <= an[i] )
        return i;

    // This point should never be reached!
    //
    throw new IllegalStateException( "Reached code that should never be reached unless there is a programming error!" );
  }
}
